Перейти к основному содержимому

6.11. Принципы компонентной архитектуры

Разработчику Архитектору Аналитику

Принципы компонентной архитектуры

Когда проект выходит за пределы одного модуля или репозитория, возникает необходимость в компонентах — независимо выпускаемых, повторно используемых единицах кода: NuGet-пакеты, npm-модули, JAR-библиотеки, Docker-образы с чётким API.

Компонент — это автономная программная единица с чётко определённым назначением, интерфейсом и поведением, способная функционировать независимо или в составе более крупной системы.

Репозиторий — централизованное хранилище исходного кода, метаданных и истории изменений, предназначенное для совместной разработки, контроля версий и управления жизненным циклом программного обеспечения.

Единица кода — минимальный логически завершённый фрагмент программного текста, реализующий конкретную функциональность и подлежащий отдельному тестированию, сборке или повторному использованию.

NuGet-пакет — дистрибутив программной библиотеки или инструмента для экосистемы .NET, содержащий скомпилированные сборки, метаданные и зависимости, распространяемый через NuGet-репозиторий.

npm-модуль — пакет программного кода, написанного на JavaScript или TypeScript, предназначенный для использования в Node.js или браузерных средах и распространяемый через реестр npm.

JAR-библиотека — архивный файл формата JAR, содержащий скомпилированный байт-код Java, ресурсы и метаданные, предназначенный для повторного использования в приложениях на платформе Java.

Docker-образ — неизменяемый шаблон файловой системы и конфигурации, описывающий среду выполнения приложения, включая зависимости, библиотеки и параметры запуска, используемый для создания контейнеров.

Роберт Мартин в «Чистой архитектуре» выделил три принципа, определяющих, как следует формировать такие компоненты. Они — аналог SOLID, но применительно к уровням выше классов: к пакетам, модулям, библиотекам.


1. REP — Reuse / Release Equivalence Principle

(Принцип эквивалентности повторного использования и выпуска)

Эквивалентность — свойство двух компонентов или реализаций, при котором они обеспечивают одинаковое поведение, интерфейс и результаты при одинаковых входных условиях.

Повторное использование — практика применения ранее разработанной программной единицы в новых контекстах без её модификации, с сохранением исходной функциональности и надёжности.

Выпуск — официальная версия программного компонента, прошедшая проверку качества, документирована и доступна для развёртывания или интеграции в другие системы.

Формулировка: Классы и модули, предназначенные для повторного использования, должны группироваться в компоненты, которые выпускаются и управляются как единое целое.

Другими словами: если вы хотите использовать некоторую функциональность в нескольких проектах, её нельзя просто копировать файлами. Она должна быть оформлена как отдельный компонент с версионированием, документированным API и процессом релиза.

Почему это важно:

  • Согласованность версий. Если модуль PaymentCore используется в трёх сервисах, и в нём исправлен баг, все три сервиса должны обновиться до одной и той же версии. Без пакетного управления легко возникнет ситуация: сервис A использует v1.2, сервис B — v1.3, сервис C — форкнутую копию. Это ведёт к расхождению поведения и трудноуловимым ошибкам.

  • Ясность контракта. У компонента есть публичный API и внутренняя реализация. Изменение публичного API требует смены мажорной версии (согласно SemVer). Это заставляет задумываться: действительно ли нужно менять интерфейс? или можно расширить его совместимым образом?

  • Ответственность за качество. Выпуск компонента — это обязательство. Перед релизом должны быть: тесты, документация, проверка обратной совместимости. Это формирует культуру.

Согласованность — соответствие поведения, структуры и интерфейсов компонентов установленным соглашениям, стандартам и ожиданиям пользователей или других систем.

Ясность — свойство компонента, при котором его назначение, интерфейс, зависимости и логика работы легко понимаются без дополнительных пояснений или анализа внутренней реализации.

Контракт — формальное или неформальное описание взаимодействия между компонентами, включающее входные и выходные данные, допустимые состояния, ошибки и правила вызова.

Качество — совокупность характеристик компонента, определяющих его пригодность к эксплуатации, включая корректность, надёжность, производительность, безопасность и удобство сопровождения.

REP говорит: если вы планируете переиспользовать — делайте это правильно. В противном случае — оставайтесь в рамках одного приложения, где управление зависимостями проще.


2. CCP — Common Closure Principle

(Принцип общей закрытости)

Закрытость — принцип проектирования, согласно которому компонент предоставляет только необходимые интерфейсы для взаимодействия, скрывая внутреннюю реализацию от внешнего влияния.

Формулировка: Классы, которые меняются по одним и тем же причинам и в одно и то же время, должны находиться в одном компоненте.

Это — масштабирование SRP (Single Responsibility Principle) на уровень компонентов. SRP говорит: у класса одна причина для изменения. CCP говорит: у компонента — один мотив для перекомпиляции и релиза.

Единственная ответственность — принцип, согласно которому компонент решает одну конкретную задачу и несёт ответственность за одно логическое назначение.

Пример. Рассмотрим систему с модулями:

  • OrderValidation — проверка корректности заказа;
  • TaxCalculation — расчёт налогов;
  • InvoiceGeneration — формирование счёта;
  • PdfRenderer — генерация PDF.

Можно сгруппировать так:

  • Компонент Billing: TaxCalculation, InvoiceGeneration, PdfRenderer
  • Компонент Orders: OrderValidation

Почему? Потому что налоговые правила, формат счёта и требования к PDF часто меняются одновременно — из-за изменений в законодательстве. Размещение их в одном компоненте означает, что обновление налоговой ставки требует одного релиза, а не трёх. При этом валидация заказа зависит от бизнес-требований к корзине и может меняться независимо — например, при введении новых типов доставки.

CCP помогает избежать двух крайностей:

  • Чрезмерной дробности — по одному классу на компонент. Тогда любое изменение требует обновления десятков пакетов, что неэффективно.
  • Монолитных компонентов — «всё в одном». Тогда даже мелкое изменение в логике валидации требует пересборки и релиза модуля, отвечающего за генерацию отчётов.

Дробность — степень разделения системы на мелкие, слабосвязанные и независимо управляемые компоненты, каждый из которых выполняет узкую функцию.

Монолитность — архитектурный подход, при котором вся функциональность системы реализована в рамках единого исполняемого модуля без явного разделения на независимые компоненты.

Ключевой вопрос при применении CCP: кто инициирует изменение? Если изменения инициируются одной командой (например, финансовой), то логика, которой владеет эта команда, должна быть в одном компоненте — даже если технически она разнородна.


3. CRP — Common Reuse Principle

(Принцип общей повторяемости)

Формулировка: Классы, не предназначенные для совместного использования, не должны находиться в одном компоненте.

Совместное использование — возможность одновременного применения одного и того же компонента несколькими системами, процессами или пользователями без конфликтов и деградации качества.

Это — аналог ISP (Interface Segregation Principle), но на уровне компонентов. Если часть компонента используется, а другая — нет, то пользователь вынужден тащить «мёртвый груз», создавая избыточные зависимости.

Пример. Допустим, есть компонент Utils, содержащий:

  • StringHelper — утилиты для работы со строками;
  • EncryptionService — шифрование данных;
  • LoggingDecorator — декоратор для логирования.

Сервису, которому нужна только StringHelper, всё равно придётся ссылаться на Utils. Если в EncryptionService появится зависимость от внешней библиотеки (например, Bouncy Castle), то все потребители Utils унаследуют эту зависимость — даже если они никогда не шифруют.

CRP требует: если часть функционала используется независимо — выносите её в отдельный компонент.

  • StringUtils
  • CryptoCore
  • Telemetry

Это снижает связанность: сервисы теперь зависят только от того, что им действительно нужно. Но есть цена: больше компонентов — больше накладных расходов на управление. Поэтому CRP применяется селективно: там, где зависимость критична (например, безопасность, лицензирование), или где повторное использование частично, но массово.


Баланс между CCP и CRP

REP, CCP и CRP не всегда совместимы. CCP толкает к объединению, CRP — к разъединению. Выбор — это компромисс, формализуемый через коэффициент устойчивости (Stability Metric), где есть число входящих зависимостей (сколько компонентов зависит от данного) и число исходящих зависимостей (от скольких компонентов зависит данный). Компонент нестабилен — ни от кого не зависит, но и никто не зависит от него (листья дерева), а компонент устойчив — на него много зависимостей, но он почти ни от кого не зависит (ядра, фреймворки).

Устойчивость — способность компонента сохранять работоспособность, корректность и предсказуемое поведение при изменениях окружения, нагрузки или частичных сбоях зависимостей.

Устойчивые компоненты (например, DomainModel, CommonTypes) должны быть спроектированы так, чтобы их интерфейсы менялись редко — иначе обновление повлечёт каскад изменений. Нестабильные (например, Adapters.Http, UI.Web) могут меняться часто — они «поглощают» нестабильность окружения.

Оптимальная структура — это направленный ациклический граф зависимостей, где устойчивые компоненты находятся внизу, нестабильные — наверху, и зависимости направлены сверху вниз. Циклов быть не должно — они означают, что границы проведены некорректно.

Оптимальность — свойство компонента, при котором он достигает наилучшего баланса между потреблением ресурсов, скоростью выполнения, читаемостью и сопровождаемостью.


Связь с DDD

Принципы компонентной архитектуры естественно ложатся на концепцию ограниченных контекстов (Bounded Contexts) из Domain-Driven Design:

  • Один ограниченный контекст — один компонент (или группа тесно связанных компонентов по CCP);
  • Публичный API контекста — порт (по гексагональной архитектуре);
  • Антикоррупционный слой (ACL) — адаптеры, реализующие интеграцию с другими контекстами;
  • Совместное использование типов между контекстами — нарушение CRP, если эти типы не являются действительно общими (например, Money, DateTimeRange).

Ограниченный контекст — область применения компонента, в пределах которой он имеет чёткое назначение, правила использования и границы ответственности, не выходящие за рамки конкретной предметной области.

В такой модели компонент — это граница семантической целостности. Внутри контекста термины имеют однозначный смысл; на стыке — требуется явное преобразование.